import { createHmac } from 'crypto';
import { deflateSync } from 'zlib';

const base64url = {
  unescape(str: string): string {
    return (str + Array(5 - (str.length % 4)))
      .replace(/_/g, '=')
      .replace(/-/g, '/')
      .replace(/\*/g, '+');
  },

  escape(str: string): string {
    return str.replace(/\+/g, '*')
      .replace(/\//g, '-')
      .replace(/=/g, '_');
  },

  encode(str: string | Buffer): string {
    const buffer = typeof str === 'string' ? Buffer.from(str) : str;
    return this.escape(buffer.toString('base64'));
  },

  decode(str: string): string {
    return Buffer.from(this.unescape(str), 'base64').toString();
  }
};

function base64encode(str: string | Buffer): string {
  const buffer = typeof str === 'string' ? Buffer.from(str) : str;
  return buffer.toString('base64');
}

function base64decode(str: string): string {
  return Buffer.from(str, 'base64').toString();
}

export class Api {
  sdkappid: number;
  key: string;

  constructor(sdkappid: number, key: string) {
    this.sdkappid = sdkappid;
    this.key = key;
  }

  private _hmacsha256(identifier: string, currTime: number, expire: number, base64UserBuf: string | null): string {
    let contentToBeSigned =
      `TLS.identifier:${identifier}\n` +
      `TLS.sdkappid:${this.sdkappid}\n` +
      `TLS.time:${currTime}\n` +
      `TLS.expire:${expire}\n`;

    if (base64UserBuf != null) {
      contentToBeSigned += `TLS.userbuf:${base64UserBuf}\n`;
    }

    return createHmac('sha256', this.key).update(contentToBeSigned).digest('base64');
  }

  private _genUserbuf(
    account: string,
    dwAuthID: number,
    dwExpTime: number,
    dwPrivilegeMap: number,
    dwAccountType: number,
    roomstr: string | null
  ): Buffer {
    const accountLength = account.length;
    const roomstrLength = roomstr ? roomstr.length : 0;
    const length = 1 + 2 + accountLength + 20 + (roomstr ? 2 + roomstrLength : 0);
    const userBuf = Buffer.alloc(length);
    let offset = 0;

    userBuf[offset++] = roomstr ? 1 : 0;

    userBuf[offset++] = (accountLength >> 8) & 0xff;
    userBuf[offset++] = accountLength & 0xff;

    for (let i = 0; i < accountLength; ++i) {
      userBuf[offset++] = account.charCodeAt(i);
    }

    [this.sdkappid, dwAuthID, Math.floor(Date.now() / 1000 + dwExpTime), dwPrivilegeMap, dwAccountType].forEach(val => {
      userBuf[offset++] = (val >> 24) & 0xff;
      userBuf[offset++] = (val >> 16) & 0xff;
      userBuf[offset++] = (val >> 8) & 0xff;
      userBuf[offset++] = val & 0xff;
    });

    if (roomstr) {
      userBuf[offset++] = (roomstrLength >> 8) & 0xff;
      userBuf[offset++] = roomstrLength & 0xff;

      for (let i = 0; i < roomstrLength; ++i) {
        userBuf[offset++] = roomstr.charCodeAt(i);
      }
    }

    return userBuf;
  }

  private genSig(userid: string, expire: number, userBuf: Buffer | null): string {
    const currTime = Math.floor(Date.now() / 1000);
    const sigDoc: Record<string, string | number> = {
      'TLS.ver': '2.0',
      'TLS.identifier': userid,
      'TLS.sdkappid': this.sdkappid,
      'TLS.time': currTime,
      'TLS.expire': expire
    };

    let sig: string;
    if (userBuf) {
      const base64UserBuf = base64encode(userBuf);
      sigDoc['TLS.userbuf'] = base64UserBuf;
      sig = this._hmacsha256(userid, currTime, expire, base64UserBuf);
    } else {
      sig = this._hmacsha256(userid, currTime, expire, null);
    }

    sigDoc['TLS.sig'] = sig;

    const compressed = deflateSync(Buffer.from(JSON.stringify(sigDoc))).toString('base64');
    return base64url.escape(compressed);
  }

  genUserSig(userid: string, expire: number): string {
    return this.genSig(userid, expire, null);
  }

  genPrivateMapKey(userid: string, expire: number, roomid: number, privilegeMap: number): string {
    const userBuf = this._genUserbuf(userid, roomid, expire, privilegeMap, 0, null);
    return this.genSig(userid, expire, userBuf);
  }

  genPrivateMapKeyWithStringRoomID(userid: string, expire: number, roomstr: string, privilegeMap: number): string {
    const userBuf = this._genUserbuf(userid, 0, expire, privilegeMap, 0, roomstr);
    return this.genSig(userid, expire, userBuf);
  }
}
